<?php

include_once(_CORE_PATH. "mapper_prototype.php");
include_once(_APP_PATH. "business/groups/groups_entity.php");
class GroupsMapper extends mapper_prototype
{
	
	static private $_allGroups = array();
	static private $_allGroupsRel = array();
	static private $_isDataLoaded = false;	
	static private $_cacheActualTime = 0;
	
	private $_user;
	private $_source;
	
	public function __construct(active_user_class $user = null)
	{
		parent::__construct('groups');
		$this->_user = $user;
		
		$this->_source = MozgSource::get('default');
	}	
	
	static public function GetInstance(active_user_class $user = null)
	{
		return new self($user);		
	}
	
	private function User()
	{ 
		if (!$this->_user) throw new Exception('Юзер не определён');
    	return $this->_user; 
	}	
		
	private function CleanCache()
	{
		self::$_allGroups = array();
		self::$_allGroupsRel = array();
		self::$_isDataLoaded = false;
		self::$_cacheActualTime = 0;		
	}

    private function GetAllData()
    {
    	if (self::$_isDataLoaded) return;
    	$this->CleanCache();
        $stmt = $this->_source->query("SELECT
							                   id,
							                   parent_id parentId,
							                   title,
							                   UNIX_TIMESTAMP(update_time) updateTime
							                FROM %%user_groups%%
							                ORDER BY parentId, id");

        $stmt->setFetchMode(PDO::FETCH_INTO, new GroupsEntity);

        foreach ($stmt as $row) 
        {
        	$sRow = clone $row;
        	
        	$id = $sRow->GetId();
        	$parentId = $sRow->GetParentId();

        	self::$_allGroups[$id] = $sRow;
            self::$_allGroupsRel[$parentId][] = $id;

        	if ($sRow->GetUpdateTime() > self::$_cacheActualTime) self::$_cacheActualTime = $sRow->GetUpdateTime();
        }		
        
        //добавляем группу суперюзера
        $superUserData = MozgCfg::GetProp('global/users//superUser');
        
		$group = $this->GetNew();
		$group->SetId($superUserData['groupId']); 
		$group->SetTitle($superUserData['groupName']);
		self::$_allGroups[$superUserData['groupId']] = $group;          
        
        self::$_isDataLoaded = true;
    }	
    
    public function IsSuperUserGroup($groupId)
    {
		return MozgCfg::GetProp('global/users//superUser/groupId') == $groupId;    	
    }
    
    public function GetNew()
    {
    	return new GroupsEntity;
    }
    
    public function GetById($id)
    {
    	$this->GetAllData();
        if (isset(self::$_allGroups[$id])) return clone self::$_allGroups[$id];
        return false;
    }    
    
    public function GetSubGroupsIds($groupIds, $allLevels = false)
    {
        if (!$groupIds) return array();
        if (!is_array($groupIds)) $groupIds = array($groupIds);

        $this->GetAllData();
        
        $groups = array();
        while (list (, $id) = each($groupIds)) 
        {
        	if (isset(self::$_allGroupsRel[$id])) 
        	{
        		$groups = array_merge($groups, self::$_allGroupsRel[$id]);
        		if ($allLevels) $groups = array_merge($groups, $this->GetSubGroupsIds(self::$_allGroupsRel[$id], true));
        	}
        }

        return array_unique($groups);
    }    
    
    public function GetParentGroupsIds($groupId)
    {
    	$parentGroupsIds = array();
    	while (($group = $this->GetById($groupId)) && ($groupId = $group->GetParentId()))
    	{
    		$parentGroupsIds[] = $groupId;
    	}
		return $parentGroupsIds;    	
    }
    
    private function LockTable(array $tableList)
    {
    	if (!is_array($tableList)) $tableList = array($tableList);
    	$sqlTableList = array();
    	foreach ($tableList as $table)
    	{
			$sqlTableList[] = $table. ' WRITE';    		
    	}
    	$this->_source->exec("LOCK TABLES " .implode(',', $sqlTableList));
    	$this->CheckCacheExpire();    	
    }

    private function UnLockTable()
    {
    	$this->_source->exec("UNLOCK TABLES");    	
    }       
    
    private function GetActualGroupsUpdateTime()
    {
        return (int)$this->_source->query("SELECT UNIX_TIMESTAMP(max(update_time)) FROM %%user_groups%%")->fetchColumn();    	
    }    
    
    private function CheckCacheExpire()
    {
    	$updateTime = $this->GetActualGroupsUpdateTime();
		if (!$updateTime || ($updateTime > self::$_cacheActualTime)) $this->CleanCache();     	
    }
    
    public function SaveGroup(GroupsEntity $obj)
    {
    	$this->LockTable(array('%%user_groups%%', '%%group_access%%'));
    	
    	$status = false;
		if ($this->Prepare($obj))
		{		
			if ($obj->GetId())
			{
				$status = $this->UpdateGroup($obj);		
			}
			else	
			{
				$status = $this->InsertGroup($obj);  
			}
		}
		
		$this->UnlockTable();
		return $status;  	
    }
    
    private function InsertGroup(GroupsEntity $obj)
    {
		$num = (bool)$this->_source->exec("insert %%user_groups%%(parent_id, title)
							     					values(
											     		  	" .$obj->GetParentId(). ",
											         		" .$this->_source->quote($obj->GetTitle()). "
											       			)
											   		");    	
    	if ($num)
    	{
    		$this->CleanCache();
			$obj->SetId($this->_source->lastInsertID());
			return true;    		
    	}
    	
    	return false;
    }
    
    private function UpdateGroup(GroupsEntity $obj)
    {    	 
		$num = (bool)$this->_source->exec("update %%user_groups%%
											       set
												        title=" .$this->_source->quote($obj->GetTitle()). ",
												        parent_id=" .$obj->GetParentId(). "
											      where id='" .$obj->GetId(). "'
											     ");    	
    	if ($num)
    	{
    		$isInParentGroups = in_array($obj->GetParentId(), $this->GetParentGroupsIds($obj->GetId()));   		
    		$this->CleanCache();
    		if (!$isInParentGroups)
    		{
    			$this->NormilizeGroupsAccess($obj->GetId());
    		}
			return true;    		
    	}
    	
    	return false;
    }    
    
	private function Prepare(GroupsEntity $obj)
	{
	    if (!strlen($obj->GetTitle())) 
	    {
            return $this->SetErrorMessage('Введите название группы');
        }

        $availableGroups = $this->User()->GetAvailableGroups();
        
        if ($obj->GetId() && !in_array($obj->GetId(), $availableGroups))
        {
	        $obj->SetId(0);
        } 

        $gr = $this->User()->GetGrantedGroups();
        $gr[] = $this->User()->GetGroupId();
        
        if (!in_array($obj->GetParentId(), array_merge($availableGroups, $gr)))
        {
        	return $this->SetErrorMessage('Не найдена родительская группа');
        	/*
        	if ($obj->GetId()) 
        	{
        		$realGroup = $this->GetById($obj->GetId());
        		if (!$realGroup) $obj->SetId(0);
        		else $obj->SetParentId($realGroup->GetParentId()); 
        	}
			else 
			{
				$obj->SetParentId($this->User()->GetGroupId());        	
			}
			*/
        }
        
        $subGroups = $this->GetSubGroupsIds($obj->GetId(), true);
        $subGroups[] = $obj->GetId();
        if (in_array($obj->GetParentId(), $subGroups))
        {
			return $this->SetErrorMessage('Группа не может принадлежать своим подгруппам');        	
        }

        return true;
	}    
    
    public function DelGroup ($id)
    {
        $id = (int)$id;
        if (!$id) return false;
         
        $group = $this->GetById($id);
        if (!$group) return false;
        
        if (!in_array($id, $this->User()->GetAvailableGroups())) return false;
        
        $num = (bool)$this->_source->exec("DELETE 
			        								ug, ugg, ga
			        							FROM %%user_groups%% ug
			        									left join %%user_granted_groups%% ugg on(ug.id=ugg.group_id)
			        									left join %%group_access%% ga on(ug.id=ga.group_id) 
			        							WHERE 
			        								ug.id='" . $id . "'");
        if ($num)
        {
            $this->_source->exec("UPDATE 
            								%%user_groups%% 
            							SET 
            								parent_id='" . $group->GetParentId() . "' 
            							WHERE 
            								parent_id='" .$group->GetId(). "'");

	        $this->CleanCache();
	        return true;
        }	        

        return false;
    }    
    
    const maxActionId = 256;
    
    /**
		возвращает двоичный код для id экшена
     */
    /**
     * 
     * @param $shift - $ctrlId
     * @param $num - $actionId
     * @return binary string
     */
    public function CreateBinCode($shift, $num)
    {
    	$num = (int)$num;
        $shift = (int)$shift;
    	if (!$num || !$shift) return "\0";
        if ($num > self::maxActionId) throw new Exception('Action Id is over limit: ' .self::maxActionId);
        
        $num --;
        $nullBytes = floor($num / 8) + ($shift - 1) * self::maxActionId / 8;
        $number = pow(2, $num % 8);
        return pack('@' . $nullBytes . 'C', $number);
    }
    
    private function NormilizeGroupsAccess($groupId, $moduleId = false)
    {
    	$addGroups = array();
    	if (!$this->IsSuperUserGroup($groupId))
    	{
	        $group = $this->GetById($groupId);
	        if (!$group) return false;
	        
	        if (!$this->IsSuperUserGroup($group->GetParentId()))
	        {
				$addGroups[] = $group->GetParentId();	        	
	        }
	        
	        $addGroups[] = $groupId;
    	}
    	
        $ga = array_merge($addGroups, $this->GetSubGroupsIds($groupId, true));
        if (!sizeof($ga)) return true;
                
        $sgAccess = $this->GetGroupsAccess($ga, $moduleId);

        $sql = '';
        while (list ($gid, $v) = each($sgAccess)) 
        {
        	$curGroup = $this->GetById($gid);
        	if (!$curGroup) continue;

        	//если родительская группа = группа суперюзера, то пропускаем текущую группуб
        	//потому что группа суперюзера не влияет на доступ
        	if ($this->IsSuperUserGroup($curGroup->GetParentId())) continue;
        	$parentGroupAccess = $sgAccess[$curGroup->GetParentId()];

            while (list($moduleId, $mAccess) = each($v)) 
            {
				if (!isset($parentGroupAccess[$moduleId]))
				{
					$parentGroupAccess[$moduleId] = $this->CreateBinCode(0, 0);
				}
				$sql .= "(" . (int)$moduleId . "," . (int)$gid . "," .$this->_source->quote($parentGroupAccess[$moduleId] & $mAccess). "),";
            }
        }

        if (strlen($sql)) 
        {
            $this->_source->exec("REPLACE %%group_access%%(module_id, group_id, maccess)
							            values
							            	" . trim($sql, ',') . "
							        ");          
        } 

        $this->_source->exec("UPDATE %%user_groups%% 
        								SET 
        									update_time=NOW() 
        								WHERE 
        									id in ('" . implode("','", $ga) . "')");        
        return true;
    }
    
    public function GetGroupsAccess($groupIds, $moduleId = false)
    {
        if (!$groupIds) return array();
        if (!is_array($groupIds)) $groupIds = array($groupIds);

        // заполняем массив $groupAccess таким образом, чтобы сохранить порядок элементов в массиве
        $groupAccess = array_fill_keys($groupIds, array());
        $stmt = $this->_source->query("SELECT
							                   module_id moduleId,
							                   maccess mAccess,
							                   group_id groupId
						                 FROM %%group_access%%
						                 WHERE
						                 	group_id in ('" . implode("','", $groupIds) . "')
						               		" .($moduleId ? " and module_id='" .(int)$moduleId . "'" : ""));

        foreach ($stmt as $row) 
        {
        	//исключаем контроллеры, к которым нет доступа
            if (!strlen(trim($row['mAccess'], "\0"))) continue;        	
            $groupAccess[$row['groupId']][$row['moduleId']] = $row['mAccess'];
        }
        
        return $groupAccess;
    } 

    private function GetMergedGroupAccess($groupIds)
    {
        $groupAccess = $this->GetGroupsAccess($groupIds);
        $resultAccess = array();
        while (list(, $access) = each($groupAccess)) 
        {
        	while (list($moduleId, $mAccess) = each($access)) 
            {          		
	            if (isset($resultAccess[$moduleId])) 
	            {
	                $resultAccess[$moduleId] |= $mAccess;
	            } 
	            else 
	            {
	                $resultAccess[$moduleId] = $mAccess;
	            }	            
            }	            
        }
        
        return $resultAccess;
    }    
    
    public function GetMergedGroupAccessFromCache($groupIds)
    {
        $accessFilename = $this->GetCacheFileName('access', (array)$groupIds);
        if (is_file($accessFilename) && (filemtime($accessFilename) > $this->GetGroupsUpdateTime((array)$groupIds)))
        {
        	return include ($accessFilename);
        }
        $groupAccess = $this->GetMergedGroupAccess($groupIds);
        $exportAccess = '<?php return ' . var_export($groupAccess, TRUE) . ';';
        if (is_file($accessFilename) && ! is_writable($accessFilename))
        {
        	@chmod($accessFilename, _CHMOD_FILE);
        }
        if ($this->Libs()->dir->Create(_CACHE_CONFIG_PATH)) file_put_contents($accessFilename, $exportAccess);
        return $groupAccess;
    }    
    
    /**
		возвращает имя файла кэша, по списку групп и префиксу файла
     */
    public function GetCacheFileName($prefix, array $groupIds)
    {
    	sort($groupIds);
        return _CACHE_CONFIG_PATH . '_' .$prefix. '_' .md5(implode(':', $groupIds)). '.php';
    }    
    
    /**
		возвращает общее время последнего обновления групп
     */
    public function GetGroupsUpdateTime(array $groupIds)
    {
    	if (!sizeof($groupIds)) return 0;
    	
    	$groupsUpdateTime = 0;
    	while (list(, $id) = each($groupIds))
    	{
			$group = $this->GetById($id);
			if (!$group) continue;
			if ($groupsUpdateTime < $group->GetUpdateTime()) $groupsUpdateTime = $group->GetUpdateTime();    		
    	}
        return $groupsUpdateTime;
    }    
    
    public function CheckAccess($accessStr, $ctrlId = false, $actionId = false)
    {
    	if ($ctrlId && $actionId)
    	{
			return (bool)strlen(trim((string)$accessStr & $this->CreateBinCode($ctrlId, $actionId), "\0")); 
    	}
    	else if ($ctrlId && !$actionId)
    	{
    		$nullBytesNum = self::maxActionId / 8;
    		return (bool)strlen(trim(substr($accessStr, ($ctrlId - 1) * $nullBytesNum, $nullBytesNum), "\0"));
    	}
    	else if (!$ctrlId && !$actionId)
    	{
    		return (bool)strlen(trim((string)$accessStr, "\0"));
    	}	  

    	throw new Exception('Invalid params');
    }
    
    public function SaveGroupAccess($groupId, $moduleId, $ctrlId, array $actions)
    {     
		if (!in_array($groupId, $this->User()->GetAvailableGroups())) return false;
    	
        try
        {
        	$allActionIds = array_keys($this->GetCtrlActionsById($moduleId, $ctrlId));
        }
        catch (Exception $e)
        {
			return false;        	
        }

        $actions = array_intersect($actions, $allActionIds);
        $sAccess = $this->CreateBinCode(0, 0);

        while (list (, $v) = each($actions)) 
        {
            $sAccess |= $this->CreateBinCode($ctrlId, $v);
        }

        $status = false;
        
        $this->LockTable(array('%%user_groups%%', '%%group_access%%'));
        
        $mAccess = $this->_source->query("SELECT 
			        								maccess 
			        							FROM 
			        								%%group_access%% 
			        							WHERE 
			        								group_id = " .$groupId. " AND
			        								module_id = " .$moduleId)->fetchColumn();
        
        if ($mAccess !== false && $mAccess !== null)
        {
        	$nullBytesNum = self::maxActionId / 8;
        	if (strlen($mAccess) >= ($ctrlId - 1) * $nullBytesNum)
        	{
        		$mAccess = substr_replace($mAccess, str_repeat("\0", $nullBytesNum), ($ctrlId - 1) * $nullBytesNum, $nullBytesNum);
        	}
        }
        else
        {
        	$mAccess = $this->CreateBinCode(0, 0);
        }

        $group = $this->GetById($groupId);
        if ($group)         
        {        
	        $num = (bool)$this->_source->exec("REPLACE %%group_access%%(module_id, group_id, maccess)
										            values
										            	('" . $moduleId . "', '" . $groupId . "', " . $this->_source->quote($mAccess | $sAccess) . ")
							           			");
	        if ($num)
	        {
	        	$this->NormilizeGroupsAccess($groupId, $moduleId);
	        	$status = true;
	        }
        }

        $this->UnlockTable();
        
        return $status;
    }    
    
    static private $_modulesById = false;
    
    public function GetModuleById($id)
    {
        if (is_array(self::$_modulesById)) 
        {
        	if (!isset(self::$_modulesById[$id])) return false;
        	return self::$_modulesById[$id];
        }
        
        self::$_modulesById = array();
        $modules = SysLoader::GetModuleCollection()->GetModuleList();
        while (list ($k, $v) = each($modules)) 
        {
            self::$_modulesById[$v['id']] = array_merge(array('name' => $k), $v);
        }
        
        return self::$_modulesById[$id];
    }    
    
    static private $_ctrlsById = array();
    
    public function GetCtrlsById($moduleId)
    {
        if (isset(self::$_ctrlsById[$moduleId])) return self::$_ctrlsById[$moduleId];              
        self::$_ctrlsById[$moduleId] = array();
        
        $moduleDetails = $this->GetModuleById($moduleId);
        if (!$moduleDetails) throw new Exception('Модуль не найден');
        
        $ctrlList = SysLoader::GetModuleCollection()->GetCtrlList($moduleDetails['name']);
        if (!is_array($ctrlList)) return array();
        
        while (list ($k, $v) = each($ctrlList)) 
        {
            self::$_ctrlsById[$moduleId][$v['id']] = array_merge(array('name' => $k), $v);
        }
        return self::$_ctrlsById[$moduleId];
    }

    static private $_ctrlActionsById = array();
    
    public function GetCtrlActionsById($moduleId, $ctrlId)
    {
        if (isset(self::$_ctrlActionsById[$moduleId][$ctrlId])) return self::$_ctrlActionsById[$moduleId][$ctrlId];
        self::$_ctrlActionsById[$moduleId][$ctrlId] = array();
        
        $moduleDetails = $this->GetModuleById($moduleId);        
        $ctrlList = $this->GetCtrlsById($moduleId);
        if (!isset($ctrlList[$ctrlId])) throw new Exception('Контроллер не найден');
        
        $actions = SysLoader::GetModuleCollection()->GetCtrlActionList($moduleDetails['name'], $ctrlList[$ctrlId]['name']);
        
        while (list ($k, $v) = each($actions)) 
        {
            self::$_ctrlActionsById[$moduleId][$ctrlId][$v['id']] = array_merge(array('name' => $k), $v);
        }
        return self::$_ctrlActionsById[$moduleId][$ctrlId];
    }     
    
}